Add Windows support with Scoop provider and POSIX compatibility shims#31
Add Windows support with Scoop provider and POSIX compatibility shims#31
Conversation
Route every POSIX-specific call in the provider stack (pwd lookup, geteuid / chown / setuid, preexec_fn, symlink_to, ``:``-joined PATH) through a new abxpkg/windows_compat.py so the same BinProvider base class works on Windows and Unix. - windows_compat.py: IS_WINDOWS / DEFAULT_PATH / UNIX_ONLY_PROVIDER_NAMES, plus shims for euid/egid/pwd, ensure_writable_cache_dir, drop_privileges preexec_fn, link_binary (symlink -> hardlink -> copy fallback), and chown_recursive (no-op on Windows). - base_types / config / binary / binprovider: PATH strings now use os.pathsep instead of hard-coded ``:``. - binprovider.py: calls the compat shims for pwd records, cache dir permissions, drop-privileges preexec_fn, and bin_dir symlinks. - ansible / pyinfra / playwright / puppeteer: route euid + chown + bin_dir shim through the same helpers. - binprovider_scoop.py: new brew-equivalent provider backed by https://scoop.sh (install / update / uninstall), registered in DEFAULT_PROVIDER_NAMES only when IS_WINDOWS. - __init__.py: filter apt/brew/nix/bash/ansible/pyinfra/docker out of the Windows default provider set, include scoop on Windows only. - CI: tests.yml gains a ``windows-latest`` / py3.13 target in the matrix, gates Nix/Bun/Yarn-Berry/linuxbrew setup on runner.os, and pins ``shell: bash`` so git-bash runs the existing setup scripts.
| def get_pw_record(self, uid: int) -> Any: | ||
| return get_pw_record(uid) |
There was a problem hiding this comment.
wtf is this shit, did you even read AGENTS.md?
There was a problem hiding this comment.
3 issues found across 15 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="abxpkg/windows_compat.py">
<violation number="1" location="abxpkg/windows_compat.py:232">
P1: Security bug: `setuid` is called before `setgid`, which is the wrong order for dropping privileges. Once the UID is dropped to a non-root user, the subsequent `setgid` call will fail (silently, due to the bare `except`), leaving the process running with the original (root) group. The standard POSIX practice is to always set GID first, then UID.</violation>
</file>
<file name=".github/workflows/tests.yml">
<violation number="1" location=".github/workflows/tests.yml:133">
P2: Don’t skip `setup-bun` on Windows; the action supports Windows and this condition unnecessarily removes Bun coverage in Windows CI.</violation>
</file>
<file name="abxpkg/binprovider_playwright.py">
<violation number="1" location="abxpkg/binprovider_playwright.py:644">
P1: The new EUID guard is wrong for Windows sentinel values: `get_current_euid()` returns `-1`, so this branch executes on Windows and then calls `os.getuid()/os.getgid()`, which crashes install flow.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
|
|
||
| def _drop() -> None: | ||
| try: | ||
| os.setuid(uid) |
There was a problem hiding this comment.
P1: Security bug: setuid is called before setgid, which is the wrong order for dropping privileges. Once the UID is dropped to a non-root user, the subsequent setgid call will fail (silently, due to the bare except), leaving the process running with the original (root) group. The standard POSIX practice is to always set GID first, then UID.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At abxpkg/windows_compat.py, line 232:
<comment>Security bug: `setuid` is called before `setgid`, which is the wrong order for dropping privileges. Once the UID is dropped to a non-root user, the subsequent `setgid` call will fail (silently, due to the bare `except`), leaving the process running with the original (root) group. The standard POSIX practice is to always set GID first, then UID.</comment>
<file context>
@@ -0,0 +1,297 @@
+
+ def _drop() -> None:
+ try:
+ os.setuid(uid)
+ os.setgid(gid)
+ except Exception:
</file context>
- pyupgrade on py3.12 CI prefers collections.abc.Callable over typing.Callable and drops Optional parens — applied the same transform locally. - binprovider_scoop.py has a #!/usr/bin/env python3 shebang and the pre-commit shebang-executable hook requires 755 for any shebanged file (matches every other binprovider_*.py).
| if self.bin_dir is None: | ||
| self.bin_dir = install_root / "shims" | ||
| self.PATH = self._merge_PATH( | ||
| install_root / "shims", |
There was a problem hiding this comment.
| install_root / "shims", | |
| install_root / "bin", |
don't call it "shims" call it "bin" like {install_root}/bin to match many of the other binproviders
Addresses review feedback from devin-ai-integration on PR #31 — three call sites still reached os.getuid() / os.getgid() on Windows after the previous refactor widened the euid guards: - binprovider_playwright.py: * needs_sudo_env_wrapper wrapped the command with /usr/bin/env KEY=VAL (non-existent on Windows). * default_install_handler chown'd install_root with os.getuid() / os.getgid(). - binprovider_puppeteer.py: _run_install_with_sudo calls os.getuid() / os.getgid() to chown the cache dir; guard the surrounding sudo-retry check with not IS_WINDOWS. - binprovider_pnpm.py: temp-store fallback path used os.getuid(); fall back to USERNAME on Windows so concurrent users still land in distinct per-user stores.
Renames the abxpkg-managed shim dir from <install_root>/shims to <install_root>/bin so ScoopProvider follows the same bin_dir convention as brew / cargo / gem / etc. Scoop's native auto-generated shim dir (<install_root>/shims/) stays on PATH so scoop-installed binaries are still resolvable, and <install_root>/apps remains as a last-resort lookup for the raw .exe paths. Addresses review feedback from @pirate on PR #31.
Two follow-ups from PR #31 review: - binprovider_pnpm.py: the fallback cache dir path must use the real UID (os.getuid()), not the effective UID. get_current_euid() wraps os.geteuid() which flips to 0 under sudo — that would silently split the pnpm store between sudo and non-sudo runs and cause cache misses. On Windows os.getuid doesn't exist, so fall back to %USERNAME%. - binprovider_scoop.py: scoop installs its shim wrappers under <install_root>/shims/, not <install_root>/bin/. The base default_abspath_handler returns None as soon as bin_dir is set and the binary isn't found there — it never falls through to self.PATH. Override default_abspath_handler with the same fall-through pattern EnvProvider uses: check bin_dir first, then self.PATH (which includes shims/ + apps/), then link the result via _link_loaded_binary so future lookups hit the managed bin/ symlink directly.
…ovider, not BinProvider ty-check / pyright caught that _link_loaded_binary is defined on EnvProvider (binprovider.py:2576), not the BinProvider base class that ScoopProvider extends. Replace the call with a direct link_binary(...) invocation (the same low-level helper _link_loaded_binary itself uses).
…path Without this guard, a second load() call after a Windows install would re-enter link_binary(abspath, abspath): the symlink-equality short-circuit only fires for symlinks, but on Windows the managed shim is typically a hardlink or copy (since symlink_to needs admin / dev mode), so it falls through to link_path.unlink() and deletes the real binary before trying to recreate it. Identified by cubic on PR #31.
… Unix-only tests Three independent Windows-compat fixes batched together since they split the failing Windows CI matrix into a much smaller set of real failures to investigate next: - abxpkg/windows_compat.py: link_binary now short-circuits when source == link_path.expanduser().absolute(). Without this, a second load() after install on Windows (where the managed shim is a hardlink or copy, not a symlink) would link_path.unlink() the only copy of the binary before trying to recreate it, leaving behind a dangling path. Identified by Devin on PR #31. - abxpkg/binprovider_scoop.py: drop the now-redundant Path(abspath) != link_path guard — the base link_binary helper handles it centrally. - abxpkg/binprovider_pip.py: virtualenvs put scripts under Scripts/ on Windows and bin/ everywhere else. Replace every hard-coded venv/bin / parent.parent.parent/bin path with a new VENV_BIN_SUBDIR constant ("Scripts" on Windows, "bin" otherwise). Fixes the test_binary, test_binprovider, test_*provider Windows failures that couldn't find pip inside a freshly-created venv. - tests/conftest.py: add collect_ignore for Unix-only provider test files when running on Windows (apt / brew / nix / bash / ansible / pyinfra / docker). The CI workflow already treats pytest exit-5 (no tests collected) as success for per-file jobs, so these files become no-ops on Windows without affecting other matrix legs.
…e suffix Three review fixes from cubic on PR #31: - tests/conftest.py: replace collect_ignore (only consulted during dir traversal) with a pytest_ignore_collect hook. The CI per-file jobs pass each test file explicitly on the command line, which bypasses collect_ignore entirely — only the hook runs for explicit paths. - binprovider_pip.py:186: use str(Path(active_venv) / VENV_BIN_SUBDIR) instead of an f"{a}/{b}" concat; other entries in pip_bin_dirs are \\-separated on Windows, so forward-slash concatenation would never match and the active venv's Scripts dir would stay in PATH. - binprovider_pip.py: Windows venvs expose python.exe / pip.exe, not python / pip. Add VENV_PYTHON_BIN / VENV_PIP_BIN constants with the .exe suffix on Windows and use them in every managed-venv lookup (is_valid, INSTALLER_BINARY, _setup_venv creation check, managed_pip resolver).
…ackages layout Two review fixes from devin-ai-integration on PR #31: - AGENTS.md: the existing "NEVER skip tests in any environment other than apt on macOS" rule predates Windows support. Document the new exception: pytest_ignore_collect skips the seven Unix-only provider test files (apt / brew / nix / bash / ansible / pyinfra / docker) on Windows since none of those providers have a Windows backend. Every other provider still runs its real install lifecycle on Windows and fails loudly. - binprovider_pip.py: Windows venvs use <venv>/Lib/site-packages (flat, no pythonX.Y/ subdir) — the old (lib).glob('python*/site-packages') glob never matched there, so PYTHONPATH stayed unset in ENV and get_cache_info missed the dist-info fingerprint. Add a venv_site_packages_dirs helper that tries the Unix versioned layout first, then falls back to the Windows flat layout, and route both call sites through it.
…IBRARY_PATH compose correctly on Windows Cubic flagged the ":" + path prefix pattern used to signal append to existing semantics to apply_exec_env: on Windows the real path separator is ;, so the old behavior produced malformed PYTHONPATH=C:\foo;C:\bar:C:\baz mixes that Python ignored. Fix the sentinel at the source instead of patching every caller: config.apply_exec_env now uses os.pathsep as BOTH the sentinel and the separator, so :"value" becomes ";value" on Windows and the resulting concatenated path-list is natively well-formed on every host. Updated all seven provider ENV composers that were passing ":" + path to pass os.pathsep + path: - binprovider_pip.py (PYTHONPATH) - binprovider_uv.py (PYTHONPATH) - binprovider_bun.py (NODE_PATH) - binprovider_npm.py (NODE_PATH) - binprovider_pnpm.py (NODE_PATH) - binprovider_yarn.py (NODE_PATH) - binprovider_nix.py (LD_LIBRARY_PATH)
…+ pip Moves VENV_BIN_SUBDIR / VENV_PYTHON_BIN / VENV_PIP_BIN / venv_site_packages_dirs (and a new scripts_dir_from_site_packages) from binprovider_pip.py into windows_compat.py so every managed-venv provider can share them. Addresses two devin-ai-integration findings on PR #31: - binprovider_uv.py was completely Unix-only: 9 hardcoded "venv" / "bin" / "python" paths + 3 Unix-only python*/site-packages globs + tool_dir/<tool>/bin/<exe> shim layout. All routed through the shared constants so uv's venv-mode resolves correctly on Windows (venv/Scripts/python.exe) and its site-packages discovery picks up the flat Windows Lib/site-packages layout. - binprovider_pip.py setup_PATH global mode walked .parent.parent.parent from site-packages to reach the scripts dir. That's right for the Unix lib/pythonX.Y/site-packages layout but overshoots by one level on Windows (Lib/site-packages is only 2 deep, producing C:\Scripts instead of C:\Python313\Scripts). The new scripts_dir_from_site_packages helper counts the right number of parents per OS.
…ookup Cubic flagged that default_abspath_handler checked (install_root / venv / Scripts / <bin_name>).exists() directly — on Windows the actual console-script executables pip / uv drop are <bin_name>.exe (and sometimes .cmd / .bat), so the bare-name check always misses them and installed tools resolve as not found. Fix: route the candidate lookup through bin_abspath which wraps shutil.which and honors PATHEXT on Windows, so every executable variant dropped by the installer is discovered. Applied to both the install_root managed-venv branch and the uv tool install branch (tool_dir / <tool_name> / Scripts / <bin_name>).
|
You're iterating quickly on this pull request. To help protect your rate limits, cubic has paused automatic reviews on new pushes for now—when you're ready for another review, comment |
…s_dir_from_site_packages Same parent-depth bug Devin flagged for setup_PATH still lived in the pip show-based abspath fallback: .parent.parent.parent / VENV_BIN_SUBDIR overshoots by one level on Windows (where Lib/site-packages is only 2 deep), producing C:\Users\user\Scripts instead of C:\Users\user\venv\Scripts. Reuse the existing scripts_dir_from_site_packages helper that counts the right number of parents per OS.
…r tests on Windows
The pytest_ignore_collect hook I added last round doesn't fire for
paths passed explicitly on the command line (pytest bypasses it for
initpaths — which is exactly how the CI per-file jobs invoke
pytest). As a result the Unix-only provider tests were still being
collected and FAILing on Windows.
Switch to pytest_collection_modifyitems which runs after collection
regardless of how items got there. On Windows we tag every item in
test_{apt,brew,nix,bash,ansible,pyinfra,docker}provider.py with a
pytest.mark.skip(reason=...) so they report as skipped (exit 0)
instead of failing.
…indows Pyright caught a second use of the Windows-only-bound linked_binary that I missed in the previous patch. Inline the is_symlink check for the provider.bin_dir / 'python3' path directly, guarded by the same not IS_WINDOWS rationale.
gem is in UNIX_ONLY_PROVIDER_NAMES on Windows so per-file
test_gemprovider.py is already skipped by conftest. This
cross-provider test_real_installs_land_under_abxpkg_lib_dir
test invokes GemProvider.install(...) directly, bypassing the
conftest filter, so gate the gem portion of its inline subprocess
script behind sys.platform != 'win32' and drop the
require_tool('gem') precondition on Windows.
The inline script's gem portion is gated on Windows but the post-script assertion loop still unconditionally expected a gem key in the returned payload — triggering KeyError: 'gem'. Match the script-side guard here so the loop only looks for gem when the script ran its gem install.
…t on Windows Third location that hardcoded gem into the expected state: the final issubset(top_level_subdirs) sanity check at the bottom of test_real_installs_land_under_abxpkg_lib_dir still listed gem even though gem is no longer installed under Windows. Wrap it with the same if not IS_WINDOWS guard.
After adding chromewebstore and gem to UNIX_ONLY_PROVIDER_NAMES in windows_compat.py, the parenthetical in AGENTS.md fell behind — it still listed only the original 7 providers. Bring the prose in sync with the actual frozenset so readers know those two providers are also skipped on Windows. Flagged by devin-ai-integration review.
…n Windows Same pattern as the goget test fix: on POSIX these providers drop the console-script shim as bin_dir/zx (or bin/cowsay), on Windows as bin_dir/zx.CMD (or Scripts/cowsay.exe). Compare .stem and .parent separately so both layouts pass without an OS branch.
…em access Fixes pyright/ty reportOptionalMemberAccess / unresolved-attribute that my previous patch introduced — the reloaded.loaded_abspath type is Path | None so accessing .parent/.stem without an explicit is not None assert flags as potentially unbound.
…s test Mirror the test_bunprovider fix: gifsicle.cmd on Windows reports the missing-vendor-binary error to stderr but still returns exit 0 (unlike POSIX shells which propagate). Accept either signal.
…brew, uv tool shim - test_pnpmprovider.py::test_install_args_win_for_ignore_scripts_and_min_release_age: same pattern as npm/bun — Windows .cmd wrappers return 0 for the --ignore-scripts postinstall-missing case but emit the is not recognized error to stderr. Accept either signal. - test_security_controls.py::test_nullable_provider_security_fields_resolve_before_handlers_run: skip the BrewProvider leg on Windows (brew is in UNIX_ONLY_PROVIDER_NAMES there and its INSTALLER_BINARY lookup raises BinProviderUnavailableError on hosts without brew, which is unrelated to what this security-field test is verifying). - test_uvprovider.py::test_global_tool_mode_can_load_and_uninstall_without_bin_shim: hardcoded tool_bin_dir / 'cowsay' misses the Windows cowsay.exe shim. Resolve via bin_abspath which honors PATHEXT so both POSIX and Windows layouts match.
Previously the Windows matrix deliberately skipped Yarn Berry because
the Unix setup uses ln -sf / homebrew prefix dirs that don't
translate. The side effect was every test_yarnprovider.py test
that uses require_tool('yarn-berry') bailed out with
AssertionError: Could not resolve the globally installed yarn-berry
alias on PATH on Windows.
Add a Windows-specific Berry setup step that uses git-bash + npm:
- npm install --prefix %USERPROFILE%/yarn-berry @yarnpkg/cli-dist@4.13.0
- write a tiny yarn-berry.cmd wrapper that forwards to the
npm-installed yarn.cmd (no ln needed).
- stage the wrapper dir onto GITHUB_PATH so shutil.which finds it.
Matches the Unix behavior (yarn classic + Berry both on PATH) so the
yarnprovider Windows test suite can actually run.
…run-update test
Fixes two Windows-only CLI regressions:
- UnicodeEncodeError: 'charmap' codec can't encode character
'\U0001f30d' — Windows console stdout defaults to the ANSI code
page (cp1252) which can't encode the emoji / box-drawing
characters abxpkg prints (🌍, 📦, —, …). Added _force_utf8_stdio
which reconfigure()s sys.stdout / sys.stderr to UTF-8
(with errors='replace' as belt-and-suspenders), wired into both
main() and abx_main() entrypoints. Unix stdio is already
UTF-8 so this is a no-op there. Fixes
test_abxpkg_version_runs_without_error and
test_version_report_includes_provider_local_cached_binary_list.
- test_run_update_skips_env_for_the_update_step: the hardcoded
Path("/tmp/fake-bin") literal stringifies differently on Windows
(\tmp\fake-bin) vs POSIX. Use tmp_path / 'fake-bin' on both
sides of the assertion so the comparison holds on every platform.
Playwright's --with-deps is a Linux apt-get-based dependency installer (and a macOS no-op). On Windows it's flat-out unsupported — Playwright prints a hard warning and ignores the flag. That warning pollutes our install log parser. Gate the flag on not IS_WINDOWS so the Windows install invocation stays clean.
npm.cmd on Windows is a batch wrapper; Python's subprocess ultimately invokes it through cmd.exe which treats > / < as redirect metacharacters. Passing zx@>=8.8.0 as an argv item gets shell-eaten to zx@ (with cmd.exe writing stdout into a file named =8.8.0), so the version pin is silently dropped and npm just reuses the already-installed zx@7.2.x, failing the subsequent min_version revalidation. Use npm's ^X.Y.Z caret range on Windows — semantically equivalent >=X.Y.Z, <X+1.0.0 upgrade range, no shell metacharacters. Applied in both default_install_handler (line 404) and default_update_handler (line 467).
The previous commit's inline heredoc put @echo off at column 1, which YAML tries to parse as the start of a token @ is a reserved indicator). GitHub Actions rejected the whole workflow as malformed, making every test job skip. Replace the heredoc with a single printf call that keeps the body inside the YAML block-scalar's indentation — functionally equivalent but parseable.
…installed via --with-deps on Windows) Corrected my earlier claim: playwright install --with-deps on Windows actually DOES install the Visual C++ 2015-2019 Redistributable (Playwright registry/dependencies.ts has explicit Windows handling for it). That's exactly the runtime chromium.exe needs to load without the WinError 14001 side-by-side configuration is incorrect error. Always emit --with-deps; update the docstring to reflect the actual per-OS behavior.
….cmd Previous setup called command -v yarn-berry / yarn-berry --version after creating yarn-berry.cmd. git-bash's command -v doesn't consult PATHEXT so it couldn't find the .cmd shim, failing the whole Setup step with exit code 1 before any tests could run. Invoke the .cmd file directly for the build-time version check, and rely on GITHUB_PATH export for subsequent steps (pytest / shutil.which on Windows does honor PATHEXT).
… layout
Windows has two distinct site-packages layouts that the function
previously conflated:
* venv / system: <prefix>/Lib/site-packages (2 parents to prefix)
* user-site: <root>/Python<ver>/site-packages (1 parent to
the versioned Python dir whose Scripts we want)
Both were being handled as site_packages.parent.parent, which for
user-site returns ...\Roaming\Python instead of
...\Roaming\Python\Python312 — so
PipProvider.setup_PATH's discovery of Windows user-installed pip
scripts fell back to sysconfig.get_path('scripts') alone.
Dispatch on the immediate parent's name (.lower() == 'lib' for
venv/system, everything else for user-site). Flagged by
devin-ai-integration on PR #31.
The abxpkg / abx CLI entrypoints call _force_utf8_stdio so they can emit the 🌍 / 📦 / etc. banner emojis without hitting cp1252 UnicodeEncodeError on Windows. The test harness's subprocess.run(capture_output=True, text=True) in _run_cli was still decoding the captured output with the parent's locale.getpreferredencoding() (cp1252 on Windows runners), which fails on emoji bytes — Python 3.13 silently sets the corresponding stream to None on the returned CompletedProcess, causing every test that does "X" in proc.stderr to raise TypeError: argument of type 'NoneType' is not iterable. Pin encoding='utf-8', errors='replace' so the parent decode matches what the child emits regardless of OS locale.
…xS dep) GitHub's windows-latest is Windows Server 2025 which ships WITHOUT the Media Feature Pack (a server-SKU optional feature). Chromium binaries bundled by Playwright / Puppeteer reference mf.dll / mfplat.dll etc. in their SxS manifests, so running chrome.exe --version after install fails with WinError 14001 side-by-side configuration is incorrect even though playwright install --with-deps successfully installs the Visual C++ Redistributable. Add Add-WindowsCapability step via pwsh to install the Media Feature Pack before provider tests run. Silently skips if already installed or unavailable.
…romium runtime); tests: portable temp dir + cmd shim Windows Chromium SxS: - Windows Server 2025 ships WITHOUT the Media Foundation role (consumer Windows has the Media Feature Pack capability, Server has the Server-Media-Foundation role). Chromium's chrome.exe SxS manifest references mf.dll / mfplat.dll from that role. Add Install-WindowsFeature -Name Server-Media-Foundation to the Windows setup step so Playwright / Puppeteer chromium binaries can actually launch after install. - Also choco install vcredist140 as belt-and-suspenders; Playwright's own --with-deps VC++ install is best-effort on some versions. Test portability: - test_run_stdout_stderr_are_separated_and_not_buffered: write a .cmd batch shim on Windows (previously only a #!/bin/sh POSIX shim — Windows can't execute that without a shell interpreter). Use os.pathsep for the PATH env override too. - abx_e2e_lib fixture: swap the hardcoded /tmp/abx-e2e-lib literal for tempfile.gettempdir()/abx-e2e-lib so the Windows runner's temp dir is used instead of C:\tmp.
Previously-pushed batch of Windows fixes picked up a formatter diff on CI that wasn't present locally — apply it now.
… path Previous attempt copied npm's yarn.cmd to a new dir, but that launcher uses %~dp0 (its own dir) to resolve yarn.js via a relative ..\@yarnpkg\cli-dist\bin\yarn.js path. After copy, %~dp0 points to the NEW dir and the relative resolution fails (Cannot find module 'C:\Users\runneradmin\@yarnpkg\...). Build the wrapper by hand: node <absolute-yarn-js-path> %*. The JS entrypoint's location is stable, so the shim works from any dir.
…rectly The node_modules/.bin/yarn file npm installs is a POSIX shell script, not a JS file — node <that> fails with a SyntaxError (missing ) after argument list). Point the wrapper straight at @yarnpkg/cli-dist/bin/yarn.js, which is the actual JS entrypoint.
The Server-Media-Foundation install step was piping through Out-Null so its result status wasn't visible. Dump it to Host so we can see whether the feature actually installed. Also install vcredist2013 (some chromium-bundled components reference the older MSVCR120.dll) alongside vcredist140. Verify System32 DLLs (mf.dll, mfplat.dll, vcruntime140.dll) at the end of the step so we can diagnose what's still missing from the SxS manifest.
…fallback pip show's stdout is occasionally truncated mid-stream on Windows when forwarded through abxpkg's capture_output=False subprocess pipe (Python subprocess pipe + Windows text-mode line translation quirk — reproduces only with pip, not other console scripts). Sometimes the Location: line doesn't make it through, so fall back to verifying black was installed into the managed venv by globbing for its black-*.dist-info directory under <tmp_path>/pip/venv/*/site-packages/. Same guarantee — the PipProvider's isolated venv is what ran pip install — without depending on flaky stdout forwarding.
- yarn/pnpm provider: use @^X.Y.Z range (not @>=X.Y.Z) on Windows. Same root cause as the earlier npm fix — yarn.cmd / pnpm.cmd go through cmd.exe, which treats ``>`` as a stdout redirect and trims ``zx@>=8.8.0`` down to ``zx@`` (eating the version floor). ^X.Y.Z has identical upgrade semantics without the metacharacter. - CI Windows yarn-berry.cmd: rewrite as a ``call`` forwarder to ``<prefix>\\node_modules\\.bin\\yarn.cmd`` (the real Yarn 4 shim npm installs). ``%~dp0`` inside that shim now resolves relative to its original dir, so yarn.js lookup works. Critically, the ``node_modules\\.bin`` dir is NOT added to PATH — keeping the globally-installed Yarn 1.x ``yarn.cmd`` visible for classic tests. - test_yarnprovider: parse the ``yarn-berry.cmd`` forwarder to recover ``berry_bin_dir`` on Windows (readlink doesn't apply to a ``.cmd`` file); use ``os.pathsep`` consistently instead of hardcoded ``:``.
…fixes
- test_puppeteerprovider / test_playwrightprovider: when ``link_binary``
appends ``.exe`` to the managed shim name on Windows, the resulting
path is ``bin_dir/chromium.exe`` (or ``chrome.exe`` for chromium),
not ``bin_dir/chromium``. Compare against the expected platform-
specific shim name instead of asserting the bare stem.
- puppeteer ``_parse_installed_browser_path``: the ``re.MULTILINE``
pattern's ``$`` doesn't consume ``\r`` in a ``\r\n`` terminator, so
on Windows the trailing ``\r`` gets captured into ``path`` and
``Path("C:\\...\\chrome.exe\r").exists()`` is False. Switch the
path group to ``[^\r\n]+?`` with trailing ``\s*`` to strip both.
- conftest ``command_version``: force UTF-8 decode on subprocess
output so emoji / non-cp1252 ``--version`` text doesn't crash the
test with UnicodeDecodeError on Windows.
Summary
This PR adds comprehensive Windows support to abxpkg by introducing a new Scoop package manager provider and a Windows compatibility layer that abstracts platform-specific operations.
Key Changes
New
windows_compat.pymodule: Centralizes all platform-specific logic with compatibility shims for:get_current_euid(),get_current_egid(),get_pw_record(),uid_has_passwd_entry())link_binary()with fallback from symlink → hardlink → copy on Windows)drop_privileges_preexec()returnsNoneon Windows instead of a callable)ensure_writable_cache_dir()skips chown/chmod on Windows)chown_recursive()no-op on Windows)PATHhandling withDEFAULT_PATHandos.pathsepusageNew
ScoopProviderclass: Windows equivalent to Homebrew that:scoop install/update/uninstallSCOOPandSCOOP_GLOBALenvironment variables<install_root>/shimsdirectoryUpdated core providers:
binprovider.py: Replaced hardcoded Unix assumptions withwindows_compatimports; usesos.pathsepfor PATH splittingbinprovider_ansible.py&binprovider_pyinfra.py: Use new compatibility functions for privilege detection and chown operationsbinprovider_playwright.py,binprovider_puppeteer.py,binprovider_npm.py,binprovider_pnpm.py,binprovider_goget.py: Importlink_binary()for cross-platform symlink handlingUpdated
__init__.py:ScoopProviderinALL_PROVIDERSUNIX_ONLY_PROVIDER_NAMESUpdated
base_types.py: Usesos.pathsepinstead of hardcoded:for PATH validationCI/CD updates (
.github/workflows/tests.yml):Implementation Details
windows_compatmodule uses sentinel values (-1for UIDs/GIDs) to signal "skip this operation" on Windows rather than raising exceptionslink_binary()gracefully degrades: tries symlink → hardlink → copy → returns source unchangedos.pathsep(:on Unix,;on Windows) for portabilityUSERNAME,USERPROFILE) are set alongside Unix equivalents for tool compatibilityPwdRecordnamedtuple provides apwd.struct_passwd-compatible interface on both platformshttps://claude.ai/code/session_01EHZ9YsbYAM7FVAwKH4nuAL
Summary by cubic
Adds first‑class Windows support with a POSIX shim layer and a new
ScoopProvider, making providers, PATH/env handling, linking, venv layouts, and CI/tests work cross‑platform. Also forces UTF‑8 stdio, always installs Playwright deps, and upgrades Windows CI (Media Foundation role, VC++ 2013 + 2015–2019 redists) so Chromium runs.New Features
windows_compat.py:IS_WINDOWS,DEFAULT_PATH,UNIX_ONLY_PROVIDER_NAMES; shims for euid/egid/pwd/chown/privileges; cross‑platformlink_binary()(symlink→hardlink→copy) with self‑link and venv‑python guards; venv helpers (VENV_*,venv_site_packages_dirs(),scripts_dir_from_site_packages()); PATH/env composition viaos.pathsep.ScoopProvider: install/update/uninstall viascoop; resolves via managedbin/, then Scoopshims/apps, linking for faster future lookups; registered only on Windows.PipProvider/UvProvideradopt WindowsScriptslayout and site‑packages discovery; console‑script resolution honorsPATHEXT;npm/pnpm/goget/playwright/puppeteeruselink_binary();EnvProvidermapspython3to the active interpreter.os.pathsep; managed shims removed on uninstall; experimentalwindows-latestCI leg (git‑bash shell); skip Unix‑only providers (incl.chromewebstore,gem); enablebun; install Yarn Berry on Windows vianpm i @yarnpkg/cli-dist@4.13.0and ayarn-berry.cmdforwarder; CRX extraction strips the header.Bug Fixes
PATHEXTfor console‑scripts; never shim venv‑rooted Python on Windows.sudo/chown;chown_recursive()is a no‑op; setUSERNAME/USERPROFILEin exec envs.stdout/stderrat startup; decode subprocess output as UTF‑8; portable temp dirs and Windows.cmdshims in buffering tests.--with-deps; Windows shim assertions are suffix‑aware; fixed CRLF path parsing for installed browser paths.@^X.Y.Zranges (not@>=X.Y.Z) inNpmProvider,YarnProvider,PnpmProviderto avoidcmd.exeredirection; accept.cmdwrapper stderr for ignore‑scripts cases; CRX extraction works without POSIXunzip.Written for commit b8769f9. Summary will update on new commits.